Skip to content

fix(orm): support DbNull/JsonNull/AnyNull for nullable typed JSON fields#2552

Merged
ymc9 merged 1 commit intodevfrom
fix/issue-2411-nullable-typed-json-null-sentinels
Apr 3, 2026
Merged

fix(orm): support DbNull/JsonNull/AnyNull for nullable typed JSON fields#2552
ymc9 merged 1 commit intodevfrom
fix/issue-2411-nullable-typed-json-null-sentinels

Conversation

@ymc9
Copy link
Copy Markdown
Member

@ymc9 ymc9 commented Apr 3, 2026

Summary

Fixes #2411

  • Create/update inputs: Nullable typed JSON fields (field: MyType? @json) now accept DbNull and JsonNull sentinels without TypeScript errors. AnyNull is rejected at the TypeScript level (no semantic meaning for mutation) and rejected at runtime.
  • Where filters: Nullable typed JSON fields now accept DbNull, JsonNull, and AnyNull passed directly (e.g. where: { field: DbNull }), matching untyped Json? field behavior.
  • Non-nullable typed JSON fields: DbNull, JsonNull, AnyNull remain rejected at the TypeScript level.

Changes

  • crud-types.ts: Added IsTypedJsonField type helper; extended ScalarFieldMutationPayload to include DbNull | JsonNull for nullable typed JSON fields; extended TypedJsonTypedFilter to include null | JsonNullValues for optional typed JSON fields in where clauses.
  • zod/factory.ts: Extracted makeNullableTypedJsonMutationSchema helper (used by both create and update schema builders); added null sentinel types to makeTypedJsonFilterSchema for optional fields; replaced inline this.schema.typeDefs && fieldDef.type in this.schema.typeDefs with existing isTypeDefType().
  • dialects/base-dialect.ts: Intercepts null sentinels passed directly in where clauses for typed JSON fields and routes them to buildJsonValueFilterClause.

Test plan

  • Regression test added at tests/regression/test/issue-2411/ using the exact schema from the issue
  • TypeScript compilation clean (tsc --noEmit)
  • Regression test passes with SQLite

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling of nullable custom JSON fields in create, update, and filter operations.
    • Fixed null value assignment and filtering logic for typed JSON fields with optional nullability.
  • Tests

    • Added regression tests for nullable typed JSON field mutations and filtering.

…lds (#2411)

Nullable typed JSON fields (e.g. `field: MyType? @json`) now correctly accept
DbNull, JsonNull, and AnyNull in create/update inputs and where filters, matching
the behavior of untyped `Json?` fields. AnyNull is rejected at the TypeScript
level for create/update inputs since it has no semantic meaning there, but is
accepted in where filters.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@claude
Copy link
Copy Markdown
Contributor

claude bot commented Apr 3, 2026

Claude finished @ymc9's task —— View job


PR Review Complete ✅

I've thoroughly reviewed this pull request and the implementation looks solid. Here's my detailed assessment:

📋 Summary

This PR successfully addresses issue #2411 by adding support for DbNull, JsonNull, and AnyNull sentinels for nullable typed JSON fields. The implementation correctly handles both mutation operations (create/update) and filtering, with proper TypeScript type safety.

🔍 Code Quality Assessment

Type Definitions (crud-types.ts) - Excellent

  • The new IsTypedJsonField helper type is well-designed
  • ScalarFieldMutationPayload extension correctly allows DbNull | JsonNull for nullable typed JSON fields while excluding AnyNull
  • TypedJsonTypedFilter enhancement properly includes null sentinels for optional fields in where clauses
  • Type logic follows existing patterns and maintains consistency

Schema Generation (zod/factory.ts) - Very Good

  • makeNullableTypedJsonMutationSchema() is a clean abstraction that properly validates null sentinels
  • The superRefine approach elegantly handles the union type issue with z.any()
  • Integration with makeTypedJsonFilterSchema() correctly adds null sentinels for optional fields
  • Code refactoring using isTypeDefType() improves maintainability

Dialect Handling (base-dialect.ts) - Good

  • Minimal but effective changes to handle null sentinels passed directly in where clauses
  • Correctly routes to existing buildJsonValueFilterClause() logic
  • Maintains separation of concerns

🧪 Test Coverage - Comprehensive

The regression test in tests/regression/test/issue-2411/ is excellent:

  • Tests all three scenarios: create, update, and find operations
  • Verifies both TypeScript compile-time errors and runtime behavior
  • Covers nullable vs non-nullable field differences
  • Tests all sentinel types (DbNull, JsonNull, AnyNull) with appropriate expectations
  • Uses the exact schema from the original issue

🔒 Security & Performance

Security: ✅ No concerns - proper input validation through Zod schemas
Performance: ✅ Minimal impact - only affects typed JSON fields and uses existing code paths

⚡ Potential Issues & Considerations

Minor Concerns:

  1. Type Complexity: The conditional types in ScalarFieldMutationPayload are complex but necessary. Consider adding inline documentation for future maintainers.

  2. Runtime vs Compile-time Consistency: The asymmetry where AnyNull is rejected at TypeScript level for mutations but accepted in filters is intentional, but could benefit from a comment explaining the design decision.

Edge Case Considerations:
3. Nested Typed JSON: The implementation handles recursive typed JSON correctly through the existing makeTypedJsonFilterSchema logic, but this should be verified with integration tests for deeply nested structures.

  1. Error Messages: Runtime rejections of AnyNull in mutations could provide more specific error messages to help developers understand why it's not supported.

🎯 Suggestions for Improvement

Low Priority:

  1. Add JSDoc comments to the new type helpers explaining their purpose
  2. Consider adding a brief comment explaining why AnyNull is excluded from mutation payloads
  3. The makeNullableTypedJsonMutationSchema could potentially be optimized to avoid the z.any() wrapper, though the current approach is correct

✅ Approval Checklist

  • Functionality: Correctly implements the required null sentinel support
  • Type Safety: Proper TypeScript compilation and type constraints
  • Code Quality: Clean, maintainable implementation following existing patterns
  • Test Coverage: Comprehensive regression test covering all scenarios
  • Backward Compatibility: No breaking changes to existing functionality
  • Documentation: Implementation aligns with PR description

🏆 Overall Assessment

This is a high-quality implementation that successfully resolves a complex type system issue. The author has:

  • Correctly identified the core problem (asymmetric null handling between untyped and typed JSON)
  • Implemented a clean solution that maintains type safety
  • Provided comprehensive test coverage
  • Made minimal, focused changes without affecting unrelated functionality

The implementation demonstrates strong understanding of the codebase architecture and TypeScript's advanced type system.

Recommendation: ✅ Approve and merge

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 3, 2026

📝 Walkthrough

Walkthrough

These changes add TypeScript typing support for null sentinel values (DbNull, JsonNull, AnyNull) when used with optional custom JSON fields in mutations and filters. Updates include refined type unions, validation schema generation, and comprehensive regression tests verifying the behavior.

Changes

Cohort / File(s) Summary
Type and Filter Refinements
packages/orm/src/client/crud-types.ts, packages/orm/src/client/crud/dialects/base-dialect.ts, packages/orm/src/client/zod/factory.ts
Updated TypedJsonTypedFilter to include JsonNullValues in optional field unions; added IsTypedJsonField type helper to detect typed-JSON fields; introduced makeNullableTypedJsonMutationSchema function to validate null sentinel instances in mutations; expanded filter validation to accept DbNullClass, JsonNullClass, and AnyNullClass for optional typed-JSON fields.
Test Schema Definitions
tests/regression/test/issue-2411/schema.zmodel, tests/regression/test/issue-2411/schema.ts, tests/regression/test/issue-2411/models.ts, tests/regression/test/issue-2411/input.ts
Added schema fixture with Metadata type definition and Foo model containing required metadata and optional optionalMetadata JSON fields; generated corresponding TypeScript type aliases, model definitions, and input/argument types.
Regression Test
tests/regression/test/issue-2411/regression.test.ts
Added test cases verifying TypeScript and runtime behavior for nullable custom JSON types; asserts that non-nullable fields reject null sentinels (compile-time errors and runtime rejections), while nullable fields accept DbNull and JsonNull but reject AnyNull; confirms null works for optional fields.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A nullable field once caused distress,
When null sentinels made TypeScript confess,
But now DbNull and JsonNull align,
The types are correct, the logic divine!
No more @ts-expect-error needed in sight,
Zenstack's custom JSON now gets it right! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main change: adding support for DbNull/JsonNull/AnyNull sentinels in nullable typed JSON fields, which directly addresses issue #2411.
Linked Issues check ✅ Passed The PR fully implements requirements from issue #2411: non-nullable typed JSON fields reject null sentinels (crud-types.ts, base-dialect.ts), nullable typed JSON fields accept DbNull/JsonNull in mutations (zod/factory.ts, crud-types.ts), and accept all sentinels in filters (base-dialect.ts, zod/factory.ts). Comprehensive regression tests verify expected behavior.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing issue #2411: type definitions, dialect logic, zod validation, and regression tests for nullable typed JSON null sentinels. No unrelated modifications detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-2411-nullable-typed-json-null-sentinels

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/orm/src/client/crud/dialects/base-dialect.ts`:
- Around line 573-577: The check for null sentinels only handles root-level
fields in the isTypeDef branch and misses nested typed-JSON recursion; update
the recursive path inside buildTypeJsonNonArrayFilter (and any helper that walks
typedef members) to detect payload instances of DbNullClass, JsonNullClass, or
AnyNullClass and route them to buildJsonValueFilterClause(fieldRef, payload)
instead of treating them as empty-object no-ops; specifically, add the same
instanceof checks and call to buildJsonValueFilterClause where nested member
payloads are processed so expressions like { metadata: { nested: DbNull } } or {
optionalMetadata: { is: JsonNull } } produce JSON-null predicates.

In `@tests/regression/test/issue-2411/regression.test.ts`:
- Around line 72-76: Seed four distinct rows via db.foo.create (or the test
suite's seeding helper) where optionalMetadata is set to values that should be
matched by each sentinel, then replace the loose findMany calls with assertions
that verify the returned ids/counts exactly match the expected row for each
sentinel: call db.foo.findMany({ where: { optionalMetadata: DbNull } }),
db.foo.findMany({ where: { optionalMetadata: JsonNull } }), db.foo.findMany({
where: { optionalMetadata: AnyNull } }) and db.foo.findMany({ where: {
optionalMetadata: null } }) and assert each returns only the seeded row(s)
intended for that sentinel (compare ids or length), so the test verifies actual
semantics of db.foo.findMany, optionalMetadata, DbNull/JsonNull/AnyNull rather
than just type acceptance.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4c92bbe6-91e3-455e-bf43-607afb207d42

📥 Commits

Reviewing files that changed from the base of the PR and between a81d8f4 and c086b69.

📒 Files selected for processing (8)
  • packages/orm/src/client/crud-types.ts
  • packages/orm/src/client/crud/dialects/base-dialect.ts
  • packages/orm/src/client/zod/factory.ts
  • tests/regression/test/issue-2411/input.ts
  • tests/regression/test/issue-2411/models.ts
  • tests/regression/test/issue-2411/regression.test.ts
  • tests/regression/test/issue-2411/schema.ts
  • tests/regression/test/issue-2411/schema.zmodel

@ymc9 ymc9 merged commit dbb3474 into dev Apr 3, 2026
11 checks passed
@ymc9 ymc9 deleted the fix/issue-2411-nullable-typed-json-null-sentinels branch April 3, 2026 22:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

(v3) Custom JSON - typescript errors when working with null values

1 participant